(in-package :lilu-app)

(defmacro with-installed-app ((app application-designator tag &key error-p) &body code)
  (let ((select-fn (intern (format nil "SELECT-~A" (string tag)))))
    `(progn
       (unless ,application-designator
         (setf ,application-designator (,select-fn)))
       (let ((,app (find-application ,application-designator ,tag ,error-p)))
         (if (and ,app (application-installed-p ,app))
             (progn
               (configure-application ,app)
               ,@code)
             (if ,error-p
                 (error "application ~A (tagged ~A) is not installed" ,application-designator ,tag)
                 (warn "application ~A (tagged ~A) is not registered or intalled" ,application-designator ,tag)))))))

(defmacro defapplication-category (name)
  (let ((var (intern (format nil "*~A*" (string name))))
        (find-fn (intern (format nil "FIND-~A" (string name))))
        (use-fn (intern (format nil "USE-~A" (string name))))
        (list-fn (intern (format nil "LIST-ALL-~AS" (string name))))
        (select-fn (intern (format nil "SELECT-~A" (string name)))))
    `(progn
       (defparameter ,var nil)
       (defun ,find-fn (designator &optional error-p)
         (find-application designator ,name error-p))
       (defun ,use-fn (string-designator &optional (error-p t))
         (with-installed-app (app string-designator ,name :error-p error-p)
           (setf ,var app)))
       (defun ,list-fn (&key installed-p)
         (list-all-applications ,name installed-p))
       (defun ,select-fn ()
         (multiple-value-bind (sel match-p)
             (dynamic-menu (format nil "select a ~A" ,name)
                           (,list-fn :installed-p t)
                           :only-match-p t)
           (when match-p
             (,use-fn sel)))))))

(defmacro defapplication-tag (name)
  (let ((find-fn (intern (format nil "FIND-~A" (string name))))
        (list-fn (intern (format nil "LIST-ALL-~AS" (string name))))
        (select-fn (intern (format nil "SELECT-~A" (string name)))))
    `(progn
       (defun ,find-fn (designator &optional error-p)
         (find-application designator ,name error-p))
       (defun ,list-fn (&key installed-p)
         (list-all-applications ,name installed-p))
       (defun ,select-fn ()
         (multiple-value-bind (sel match-p)
             (dynamic-menu (format nil "select a ~A" ,name)
                           (,list-fn :installed-p t)
                           :only-match-p t)
           (when match-p
             sel))))))

;;;
;;; repl application
;;;

(defapplication-category :repl)

(defgeneric run-open-repl (application &key error-p wait-p))

(defun open-repl (&key (repl *repl*) (error-p t) wait-p)
  (with-installed-app (app repl :repl :error-p error-p)
    (run-open-repl app :error-p error-p :wait-p wait-p)))


;;;
;;; editor application
;;;

(defapplication-category :editor)

(defgeneric run-open-editor (application &key file nline error-p wait-p))

(defun open-editor (&key (file nil) (nline nil) (editor *editor*)
                      (error-p t) wait-p)
  (with-installed-app (app editor :editor :error-p error-p)
    (run-open-editor app :file file :nline nline :error-p error-p :wait-p wait-p)))

(defun edit (file &key nline (error-p t) wait-p)
  (open-editor :file file :nline nline :error-p error-p :wait-p wait-p))

;;;
;;; browser application
;;;

(defapplication-category :browser)

(defgeneric run-open-browser (application &key url error-p wait-p))

(defun open-browser (&key (url nil) (browser *browser*) (error-p t) wait-p)
  (with-installed-app (app browser :browser :error-p error-p)
    (run-open-browser app :url url :error-p error-p :wait-p wait-p)))

(defun view-web-page (url &key (error-p t) wait-p)
  (open-browser :url url :browser *browser* error-p error-p :wait-p wait-p ))

;;;
;;; terminal application
;;;

(defapplication-category :terminal)

(defgeneric run-open-terminal (application &key cmd error-p wait-p))

(defun open-terminal (&key (cmd nil) (terminal *terminal*) (error-p t) wait-p)
  (with-installed-app (app terminal :terminal :error-p error-p)
    (run-open-terminal app :cmd cmd :error-p error-p :wait-p wait-p)))

(defun exec-terminal-command (cmd &key (error-p t) wait-p)
  (open-terminal :cmd cmd :error-p error-p :wait-p wait-p))

;;;
;;; dmenu applications
;;;

(defapplication-category :dmenu)

(defgeneric run-dynamic-menu (application prompt sequence &rest args &key item-fn only-match-p
                                                                       force-only-match-p
                                                                       case-sensitive-p
                                                                       error-p))

(defun dynamic-menu (prompt sequence &key (dmenu *dmenu*) (item-fn #'identity) (only-match-p nil)
                                       (force-only-match-p nil) (case-sensitive-p nil) (error-p t))
  (with-installed-app (app dmenu :dmenu :error-p error-p)
    (run-dynamic-menu (find-application dmenu :dmenu t)
                      prompt sequence
                      :item-fn item-fn
                      :only-match-p only-match-p
                      :force-only-match-p force-only-match-p
                      :case-sensitive-p case-sensitive-p
                      :error-p error-p)))

;;;
;;; debugger application
;;;

(defapplication-category :debugger)

(defgeneric get-debugger-hook (application))
(defmethod get-debugger-hook ((application simple-debugger-application-mixin))
  (with-slots (debugger-hook) application
    debugger-hook))

(defgeneric run-invoke-debugger-hook (application condition me-or-my-encapsulation))
(defmethod run-invoke-debugger-hook ((application application)
                                     condition me-or-my-encapsulation)
  (funcall (lilu-app:get-debugger-hook application)
           condition me-or-my-encapsulation))

(defun invoke-debugger-hook (condition me-or-my-encapsulation
                             &key (debugger *debugger*))
  (with-installed-app (app debugger :debugger :error-p nil)
    (when app
      (run-invoke-debugger-hook app condition me-or-my-encapsulation))))

;;;
;;; inspector application
;;;

(defapplication-category :inspector)

(defgeneric run-open-inspector (application object &key error-p wait-p))

(defun open-inspector (object &key (inspector *inspector*) (error-p t) wait-p)
  (with-installed-app (app inspector :inspector :error-p error-p)
    (run-open-inspector app object :error-p error-p :wait-p wait-p)))

(defun inspect-object (object &key (error-p t) wait-p)
  (open-inspector object :error-p error-p :wait-p wait-p))

;;;
;;; app
;;;

(defapplication-tag :app)

(defgeneric run-open-app (application &rest args &key error-p wait-p &allow-other-keys))

(defun open-app (app-designator &rest args &key wait-p error-p &allow-other-keys)
  (declare (ignore wait-p))
  (with-installed-app (app app-designator :app :error-p error-p)
    (apply #'run-open-app app args)))

;;;
;;; simple classes
;;;

(defclass simple-program-app (simple-program-application-mixin
                              application)
  ())

(defmethod run-open-app ((app simple-program-app) &rest args &key error-p wait-p &allow-other-keys)
  (declare (ignore wait-p error-p))
  (apply #'run-simple-shell-program app args))

(defclass simple-terminal-app (simple-program-application-mixin
                               application)
  ())

(defmethod run-open-app ((app simple-terminal-app) &rest args &key error-p wait-p &allow-other-keys)
  (exec-terminal-command (apply #'make-simple-program-shell-command app args)
                         :error-p error-p :wait-p wait-p))

(defclass simple-cl-app (simple-cl-application-mixin
                         application)
  ())

(defmethod run-open-app ((app simple-cl-app) &rest args &key error-p wait-p &allow-other-keys)
  (declare (ignore error-p))
  (with-slots (entry-fn name) app
    (if entry-fn
        (if wait-p
            (apply entry-fn app args)
            (apply #'launch-application app entry-fn args))
        (progn
          (warn "Entry function for ~A undefined" name)
          (error "Entry function for ~A undefined" name)))))

;;;
;;; standard apps
;;;

(register-application
 (make-instance 'simple-cl-app
                :name :terminal
                :pretty-name "Terminal"
                :system-name "lilu-terminal"
                :entry-fn #'(lambda (app &key cmd wait-p error-p)
                              (declare (ignore app))
                              (open-terminal :cmd cmd :wait-p wait-p :error-p error-p))
                :tags '(:app)))

(register-application
 (make-instance 'simple-cl-app
                :name :browser
                :pretty-name "Browser"
                :system-name "lilu-browser"
                :entry-fn #'(lambda (app &key url wait-p error-p)
                              (declare (ignore app))
                              (open-browser :url url :wait-p wait-p :error-p error-p))
                :tags '(:app)))

(register-application
 (make-instance 'simple-cl-app
                :name :repl
                :pretty-name "Repl"
                :entry-fn #'(lambda (app &key wait-p error-p)
                              (declare (ignore app))
                              (open-repl :wait-p wait-p :error-p error-p))
                :tags '(:app)))

(register-application
 (make-instance 'simple-cl-app
                :name :editor
                :pretty-name "Editor"
                :system-name "lilu-emacs"
                :entry-fn #'(lambda (app &key file wait-p error-p)
                              (declare (ignore app))
                              (open-editor :file file :wait-p wait-p :error-p error-p))
                :tags '(:app)))

(register-application
 (make-instance 'simple-cl-app
                :name :inspector
                :pretty-name "Inspector"
                :entry-fn #'(lambda (app &key object wait-p error-p)
                              (declare (ignore app))
                              (open-inspector object :wait-p wait-p :error-p error-p))
                :tags '(:app)))
